<?php

namespace WDB\Wrapper;

use WDB,
    WDB\Exception,
    WDB\Query\Element;

/**
 * @author Richard Ejem <richard(at)ejem.cz>
 * @package WDB
 */
class TimeDependentRecord extends Record
{
    /**@var bool $isNewTimespan*/
    protected $isNewTimespan = FALSE;
    /**@var \DateTime|null $fromGlobal*/
    protected $fromGlobal;
    /**@var \DateTime|null $fromGlobalOriginal*/
    protected $fromGlobalOriginal;
    /**@var \DateTime|null $toGlobal*/
    protected $toGlobal;
    /**@var \DateTime|null $toGlobalOriginal*/
    protected $toGlobalOriginal;

    /**@var bool internal switch to suppress timespan integrity mechanism used only when saving an inner record*/
    private $doNotUpdateGlobalTimeRanges = FALSE;

    // <editor-fold defaultstate="collapsed" desc="iRecord implementation">
    public function __construct(Structure\RecordInitializer $ri)
    {
        parent::__construct($ri);
        $this->fromGlobal = $this->fromGlobalOriginal = $ri->row[TimeDependentTable::FROM_COLUMN_GLOBAL_ALIAS];
        $this->toGlobal = $this->toGlobalOriginal = $ri->row[TimeDependentTable::TO_COLUMN_GLOBAL_ALIAS];
        $this->isNewTimespan = $this->isNew;
    }
    public function delete()
    {
        if ($this->isNew)
        {
            throw new Exception\InvalidOperation("cannot delete newly created row.");
        }
        $queryMaster = new WDB\Query\Delete($this->tableWrapper->getTableAnalyzer()->getIdentifier(), $this->keyMatchConditionMasterTable());
        $queryTd = new WDB\Query\Delete($this->tableWrapper->getTimeTableAnalyzer()->getIdentifier(), $this->keyMatchConditionMasterTable());//intentionally master table - purge all bound time dependency records

        //deleting the record - current object should behave as a new record
        $this->isNew = TRUE;

        return $queryMaster->run($this->tableWrapper->database)->success && $queryTd->run($this->tableWrapper->database)->success;
    }

    public function getUniqueIdentificationKey()
    {
        return $this->getMasterIdentificationKey();
    }

    public function getMasterIdentificationKey()
    {
        if ($this->isNew()) return NULL;
        $val = array();
        foreach ($this->tableWrapper->getMasterPrimaryKey() as $part)
        {
            $val[$part] = $this->fields[$part]->getOriginalValue();
        }
        return $val;
    }

    public function getIdentificationKey()
    {
        if ($this->isNew()) return NULL;
        return array(WDB\GTO\WsqlLanguage::quoteIdentifier(Element\ColumnIdentifier::create(TimeDependentTable::ID_COLUMN,
                $this->tableWrapper->getTimeTableAnalyzer()->getName(),
                $this->tableWrapper->getTimeTableAnalyzer()->getSchema()->getName()))=> $this->fields[TimeDependentTable::ID_COLUMN]->getOriginalValue());
    }
    // </editor-fold>

    /**
     *
     * @return WDB\Query\WriteResult
     */
    protected function saveNew()
    {
        $rowMaster = array();
        $rowTd = array();
        foreach ($this->fields as $field)
        {
            if ($field->isWritten())
            {
                if (in_array($field->getColumn(), $this->tableWrapper->getMasterColumns()))
                {
                    $field->saveToRow($rowMaster);
                }
                if (in_array($field->getColumn()->getName(), $this->tableWrapper->getPrimaryKey())
                    || in_array($field->getColumn(), $this->tableWrapper->getTimeDependentColumns()))
                {
                    $field->saveToRow($rowTd);
                }
            }
        }
        $queryMaster = new WDB\Query\Insert($this->tableWrapper->getTableAnalyzer()->getIdentifier(), $rowMaster, $this->ODKUmode);
        $this->tableWrapper->getDatabase()->startTransaction();
        $resultM = $queryMaster->run($this->tableWrapper->getDatabase());
        if ($resultM->insertId)
        {
            foreach ($this->tableWrapper->getMasterColumns() as $column)
            {
                /**@var $column WDB\Wrapper\iColumn*/
                if ($column->isAutoIncrement())
                {
                    $this->getField($column->getName())->setValue($resultM->insertId)->saveToRow($rowTd);

                }
            }
        }
        $queryTd = new WDB\Query\Insert($this->tableWrapper->getTimeTableAnalyzer()->getIdentifier(), $rowTd, WDB\Query\Insert::UNIQUE);
        $resultT = $queryTd->run($this->tableWrapper->getDatabase());
        if (!$resultM->success || !$resultT->success || !$this->updateGlobalTimeRanges())
        {
            $this->tableWrapper->getDatabase()->rollback();
            return FALSE;
        }

        $this->tableWrapper->getDatabase()->commit();

        $this->isNew = $this->isNewTimespan = FALSE;

        $this->getField('id~td')->setValue($resultT->insertId);
        return TRUE;
    }

    /**
     *
     * @return WDB\Query\WriteResult
     */
    protected function saveUpdate()
    {
        $timeTableName = $this->tableWrapper->getTimeTableAnalyzer()->getIdentifier();
        $rowMaster = array();
        $rowTd = array();
        foreach ($this->fields as $field)
        {
            $isFromTd = (in_array($field->getColumn()->getName(), $this->tableWrapper->getMasterPrimaryKey())
                        || in_array($field->getColumn(), $this->tableWrapper->getTimeDependentColumns()))
                    && $field->getColumn()->getName() !== TimeDependentTable::ID_COLUMN;
            $isFromMaster = in_array($field->getColumn(), $this->tableWrapper->getMasterColumns());
            if ($this->writeMode == iRecord::WM_WRITTEN && $field->isWritten() ||
                    $this->writeMode == iRecord::WM_CHANGED && $field->isChanged() ||
                    $this->writeMode == iRecord::WM_ALL ||
                    $isFromTd && $this->isNewTimespan)
            {
                if ($isFromMaster)
                {
                    $field->saveToRow($rowMaster);
                }
                if ($isFromTd)
                {
                    $field->saveToRow($rowTd);
                }
            }
        }
        $queryMaster = new WDB\Query\Update($this->tableWrapper->getTableAnalyzer()->getIdentifier(), $rowMaster);
        $queryMaster->where = $this->keyMatchConditionMasterTable();
        if ($this->isNewTimespan)
        {
            $queryTd = new WDB\Query\Insert($timeTableName, $rowTd, WDB\Query\Insert::UNIQUE);
        }
        else
        {
            $queryTd = new WDB\Query\Update($timeTableName, $rowTd);
            $queryTd->where = $this->keyMatchConditionTdTable();
        }
        $this->tableWrapper->getDatabase()->startTransaction();
        $resultM = $queryMaster->run($this->tableWrapper->getDatabase());
        $resultT = $queryTd->run($this->tableWrapper->getDatabase());
        if ($this->isNewTimespan() && $resultT->success)
        {
            $this->getField(TimeDependentTable::ID_COLUMN)->setValue($resultT->insertId);
        }
        if ($resultM->success
                && $resultT->success
                && ($this->doNotUpdateGlobalTimeRanges ||$this->updateGlobalTimeRanges()))
        {
            $this->tableWrapper->getDatabase()->commit();
            return TRUE;
        }
        else
        {
            $this->tableWrapper->getDatabase()->rollback();
            return FALSE;
        }
    }

    //<editor-fold defaultstate="collapsed" desc="timespan consistency methods">
    private function deleteCoveredTimespans()
    {
        $cond = new Element\LogicOperator(
                Element\Compare::NEquals(
                        Element\ColumnIdentifier::create(TimeDependentTable::ID_COLUMN),
                        Element\Datatype\AbstractType::createDatatype($this[TimeDependentTable::ID_COLUMN])
                        ),
                Element\LogicOperator::L_AND);
        //overlapped by current timespan (F>=MIN(Fnew, Forig) && T <= MAX(Tnew,Torig))
        $from = $this[TimeDependentTable::FROM_COLUMN];
        $fromOrig = $this->getField(TimeDependentTable::FROM_COLUMN)->getOriginalValue();
        if ($from !== NULL && ($fromOrig !== NULL || $this->isNewTimespan))
        {
            $delThreshold = $this[TimeDependentTable::FROM_COLUMN];
            if (!$this->isNewTimespan)
            {
                $delThreshold = min($from, $fromOrig);
            }
            $cond->addExpression(Element\Compare::Gte(
                        Element\ColumnIdentifier::create(TimeDependentTable::FROM_COLUMN),
                        Element\Datatype\AbstractType::createDatatype($delThreshold)
                        ));
        }
        $to = $this[TimeDependentTable::TO_COLUMN];
        $toOrig = $this->getField(TimeDependentTable::TO_COLUMN)->getOriginalValue();
        if ($to !== NULL && ($toOrig !== NULL || $this->isNewTimespan))
        {
            $delThreshold = $this[TimeDependentTable::TO_COLUMN];
            if (!$this->isNewTimespan)
            {
                $delThreshold = max($to, $toOrig);
            }
            $cond->addExpression(Element\Compare::Lte(
                        Element\ColumnIdentifier::create(TimeDependentTable::TO_COLUMN),
                        Element\Datatype\AbstractType::createDatatype($delThreshold)
                        ));
        }
        //cut-out with reduced global range (T<Fglobal || F > Tglobal)
        if ($this->fromGlobal !== NULL || $this->toGlobal !== NULL)
        {
            $cond = new Element\LogicOperator(array($cond), Element\LogicOperator::L_OR);
            if ($this->fromGlobal !== NULL)
            {
                $cond->addExpression(Element\Compare::Lte(
                            Element\ColumnIdentifier::create(TimeDependentTable::TO_COLUMN),
                            Element\Datatype\AbstractType::createDatatype($this->fromGlobal)
                            ));
            }
            if ($this->toGlobal !== NULL)
            {
                $cond->addExpression(Element\Compare::Gte(
                            Element\ColumnIdentifier::create(TimeDependentTable::FROM_COLUMN),
                            Element\Datatype\AbstractType::createDatatype($this->toGlobal)
                            ));
            }
        }
        $timeTableName = $this->tableWrapper->getTimeTableAnalyzer()->getIdentifier();
        $q=new WDB\Query\Delete($timeTableName, $cond);
        $q->filter($this->getMasterIdentificationKey())->not($this->getIdentificationKey());
        return $this->tableWrapper->getDatabase()->query($q)->success;
    }

    private function fitNeighbourTimespans()
    {
        $timeTableName = $this->tableWrapper->getTimeTableAnalyzer()->getIdentifier();
        if ( (  $this->getField(TimeDependentTable::FROM_COLUMN)->isChanged()
                && $this->getField(TimeDependentTable::FROM_COLUMN)->getOriginalValue() !== NULL
                && $this[TimeDependentTable::FROM_COLUMN] !== NULL
             ) || $this->isNewTimespan)
        {
            $toCol = Element\ColumnIdentifier::create(TimeDependentTable::TO_COLUMN);
            $getNearestLowerTime = new WDB\Query\Select(
                    array(
                        new Element\DBFunction('MAX', array($toCol)),
                        $toCol),
                    $timeTableName
            );
            $getNearestLowerTime->addCondition(Element\Compare::Lte($toCol,
                new Element\Datatype\DateTime($this[TimeDependentTable::FROM_COLUMN])));
            $getNearestLowerTime->filter($this->getMasterIdentificationKey());
            $nearestLowerTime = $getNearestLowerTime->run($this->tableWrapper->getDatabase());
            if ($nearestLowerTime->count())
            {
                $q = new WDB\Query\Update($timeTableName,
                    array(TimeDependentTable::TO_COLUMN=>$this[TimeDependentTable::FROM_COLUMN]),
                    Element\Compare::Equals(
                            Element\ColumnIdentifier::create(TimeDependentTable::TO_COLUMN),
                            new Element\Datatype\DateTime($nearestLowerTime->singleValue())
                        )
                    );
                if (!$q->run($this->tableWrapper->getDatabase())->success) return FALSE;
            }
        }
        if ( (  $this->getField(TimeDependentTable::TO_COLUMN)->isChanged()
                && $this->getField(TimeDependentTable::TO_COLUMN)->getOriginalValue() !== NULL
                && $this[TimeDependentTable::TO_COLUMN] !== NULL
             ) || $this->isNewTimespan)
        {
            $fromCol = Element\ColumnIdentifier::create(TimeDependentTable::FROM_COLUMN);
            $getNearestGreaterTime = new WDB\Query\Select(
                    array(
                        new Element\DBFunction('MIN', array($fromCol)),
                        $fromCol),
                    $timeTableName
            );
            $getNearestGreaterTime->addCondition(Element\Compare::Gte($fromCol,
                    new Element\Datatype\DateTime($this[TimeDependentTable::TO_COLUMN])));
            $getNearestGreaterTime->filter($this->getMasterIdentificationKey());
            $nearestGreaterTime = $getNearestGreaterTime->run($this->tableWrapper->getDatabase());
            if ($nearestGreaterTime->count())
            {
                $q = new WDB\Query\Update($timeTableName,
                        array(TimeDependentTable::FROM_COLUMN=>$this[TimeDependentTable::TO_COLUMN]),
                        Element\Compare::Equals(
                                Element\ColumnIdentifier::create(TimeDependentTable::FROM_COLUMN),
                                new Element\Datatype\DateTime($nearestGreaterTime->singleValue())
                            )
                        );
                if (!$q->run($this->tableWrapper->getDatabase())->success) return FALSE;
            }
        }
        return TRUE;
    }

    private function getSurroundingRecord()
    {
        //get record characterised as  from<this.from & to>this.to

        if ($this[TimeDependentTable::FROM_COLUMN] === NULL || $this[TimeDependentTable::TO_COLUMN] === NULL) return NULL;
        $tt = WDB\GTO\WsqlLanguage::quoteIdentifier($this->tableWrapper->getTimeTableAnalyzer()->getSchema()->getName()).'.'.
              WDB\GTO\WsqlLanguage::quoteIdentifier($this->tableWrapper->getTimeTableAnalyzer()->getName());

        $from = WDB\GTO\WsqlLanguage::quoteLiteral($this[TimeDependentTable::FROM_COLUMN]);
        $to = WDB\GTO\WsqlLanguage::quoteLiteral($this[TimeDependentTable::TO_COLUMN]);

        $fc = WDB\GTO\WsqlLanguage::quoteIdentifier(TimeDependentTable::FROM_COLUMN);
        $tc = WDB\GTO\WsqlLanguage::quoteIdentifier(TimeDependentTable::TO_COLUMN);
        $rec = $this->table->getAllTimesDatasource()
                ->filter($this->getMasterIdentificationKey())
                ->addCondition(WDB\GTO\WsqlLanguage::parse("($tt.$fc < $from OR $tt.$fc = NULL) AND ($tt.$tc > $to  OR $tt.$tc = NULL)", 'Condition'))
                ->run($this->tableWrapper->getDatabase());
        if($rec->count())
        {
            $r = $this->tableWrapper->elevateRecords(array($rec->singleRow()));
            return $r[0];
        }
        else
        {
            return NULL;
        }
    }

    private function splitSurroundingRecord(TimeDependentRecord $surroundingRecord)
    {
        $originalToColumn = $surroundingRecord[TimeDependentTable::TO_COLUMN];
        $surroundingRecord[TimeDependentTable::TO_COLUMN] = $this[TimeDependentTable::FROM_COLUMN];
        $surroundingRecord->doNotUpdateGlobalTimeRanges = TRUE;
        $surroundingRecord->validateUponSave = FALSE;
        if (!$surroundingRecord->save()) return FALSE;
        $surroundingRecord->makeNewTimespan($this[TimeDependentTable::TO_COLUMN], $originalToColumn);
        if (!$surroundingRecord->save()) return FALSE;
        return TRUE;
    }

    private function applyGlobalBoundary($originalValue, $newValue, $columnName, $dbfuncName)
    {
        if ($originalValue !== $newValue)
        {
            $timeTableName = $this->tableWrapper->getTimeTableAnalyzer()->getIdentifier();
            $colId = Element\ColumnIdentifier::create($columnName);
            $getTimeBoundary = new WDB\Query\Select(
                    array (
                        new Element\SelectField(new Element\DBFunction($dbfuncName, array($colId)), 'bnd'),
                        new Element\SelectField(new Element\DBFunction('SUM', array(Element\Compare::Equals($colId, new Element\Datatype\TNull()))), 'hasNull')
                    ),
                    $timeTableName);
            $getTimeBoundary->filter($this->getMasterIdentificationKey());
            $bndTime = $getTimeBoundary->run()->singleRow();
            if ($bndTime['hasNull'])
            {
                $lt = new Element\Datatype\TNull();
            }
            else
            {
                $lt = new Element\Datatype\DateTime($bndTime['bnd']);
            }
            //TODO HERE
            $q = new WDB\Query\Update($timeTableName,
                    array($columnName=>$newValue),
                    Element\Compare::Equals(
                            Element\ColumnIdentifier::create($columnName),
                            $lt
                        )
                    );
            if (!$q->run($this->tableWrapper->getDatabase())->success) return FALSE;
        }
        return TRUE;
    }

    private function applyGlobalRange()
    {
        return $this->applyGlobalBoundary($this->fromGlobalOriginal, $this->fromGlobal, TimeDependentTable::FROM_COLUMN, 'MIN')
            && $this->applyGlobalBoundary($this->toGlobalOriginal,   $this->toGlobal,   TimeDependentTable::TO_COLUMN, 'MAX');
    }

    private function updateGlobalTimeRanges()
    {
        $timespanChanged = $this->isNewTimespan
                || $this->getField(TimeDependentTable::FROM_COLUMN)->isChanged()
                || $this->getField(TimeDependentTable::TO_COLUMN)->isChanged();
        $globalChanged = $this->fromGlobal !== $this->fromGlobalOriginal || $this->toGlobal !== $this->toGlobalOriginal;
        if ($timespanChanged || $globalChanged)
        {
            if (!$this->deleteCoveredTimespans()) return FALSE;
        }
        if ($timespanChanged)
        {
            if (($surroundingRecord = $this->getSurroundingRecord()) !== NULL)
            {
                if (!$this->splitSurroundingRecord($surroundingRecord)) return FALSE;
            }
            else
            {
                if (!$this->fitNeighbourTimespans()) return FALSE;
            }
        }
        if ($globalChanged)
        {
            if(!$this->applyGlobalRange()) return FALSE;
        }
        return TRUE;
    }
    //</editor-fold>


    protected function keyMatchCondition()
    {
        return $this->keyMatchConditionTdTable();
    }

    /**
     * get a where condition to identify current record in a master table.
     *
     * @return \WDB\Query\Element\iCondition
     */
    protected function keyMatchConditionMasterTable()
    {
        $ikey = array();
        foreach ($this->tableWrapper->getMasterPrimaryKey() as $part)
        {
            $ikey[$part] = $this->fields[$part]->getOriginalValue();
        }
        return $this->matchConditionForKey($ikey);
    }

    /**
     * get a where condition to identify current record in a time dependency support table.
     *
     * @return \WDB\Query\Element\iCondition
     */
    protected function keyMatchConditionTdTable()
    {
        return $this->matchConditionForKey(array(TimeDependentTable::ID_COLUMN=>$this[TimeDependentTable::ID_COLUMN]));
    }

    protected function matchConditionForKey($key)
    {
        $compare = array();
        $columns = $this->tableWrapper->getColumns();

        foreach ($key as $k=>$val)
        {
            $compare[] = WDB\Query\Element\Compare::Equals(
                            WDB\Query\Element\ColumnIdentifier::create($k), $columns[$k]->valueToDatatype($val)
            );
        }
        return WDB\Query\Element\LogicOperator::lAnd($compare);
    }

    //<editor-fold defaultstate="collapsed" desc="timespan controls">
    protected function isChangedTimespan()
    {
        return $this->getField(TimeDependentTable::FROM_COLUMN)->isChanged()
                || $this->getField(TimeDependentTable::TO_COLUMN)->isChanged();
    }

    protected function isNewTimespan()
    {
        return $this->isNew || $this->isNewTimespan;
    }

    /**
     * Left boundary of this record's timespan
     *
     * @return \DateTime|NULL null means -infinity
     */
    public function getFrom()
    {
        return $this[TimeDependentTable::FROM_COLUMN];
    }

    /**
     * Right boundary of this record's timespan
     *
     * @return \DateTime|NULL null means +infinity
     */
    public function getTo()
    {
        return $this[TimeDependentTable::TO_COLUMN];
    }

    /**
     * Set left boundary of this record's timespan
     *
     * @param int|\DateTime|string|NULL $value
     * @return self fluent interface
     */
    public function setFrom($value)
    {
        $this[TimeDependentTable::FROM_COLUMN] = WDB\Utils\System::createDateTime($value);
        return $this;
    }

    /**
     * Set right boundary of this record's timespan
     *
     * @param int|\DateTime|string|NULL $value
     * @return self fluent interface
     */
    public function setTo($value)
    {
        $this[TimeDependentTable::TO_COLUMN] = WDB\Utils\System::createDateTime($value);
        return $this;
    }

    /**
     *
     * @return \DateTime|null
     */
    public function getFromGlobal()
    {
        return $this->fromGlobal;
    }

    /**
     *
     * @return \DateTime|null
     */
    public function getToGlobal()
    {
        return $this->toGlobal;
    }


    /**
     * Set global left boundary of this record's validity (first timespan's begin)
     *
     * @param int|\DateTime\string\NULL|bool $value - like setFrom, false means do not modify
     * @return self fluent interface
     */
    public function setGlobalFrom($value)
    {
        if ($value === FALSE) $this->fromGlobal = $this->fromGlobalOriginal;
        else $this->fromGlobal = WDB\Utils\System::createDateTime($value);
        return $this;
    }

    /**
     * Set global right boundary of this record's validity (last timespan's end)
     *
     * @param int|\DateTime\string\NULL|bool $value - like setTo, false means do not modify
     * @return self fluent interface
     */
    public function setGlobalTo($value)
    {
        if ($value === FALSE) $this->toGlobal = $this->toGlobalOriginal;
        else $this->toGlobal = WDB\Utils\System::createDateTime($value);
        return $this;
    }

    /**
     * Set both boundaries of this record's timespan
     *
     * @param int|\DateTime\string\NULL $from
     * @param int|\DateTime\string\NULL $to
     * @return self fluent interface
     */
    public function setTimespan($from, $to)
    {
        return $this->setFrom($from)->setTo($to);
    }

    /**
     * Set both global boundaries of this record
     *
     * @param int|\DateTime\string\NULL $from
     * @param int|\DateTime\string\NULL $to
     * @return self fluent interface
     */
    public function setGlobalTimespan($from, $to)
    {
        return $this->setGlobalFrom($from)->setGlobalTo($to);
    }

    /**
     * Specifies that this record will be saved as new timespan (not overwrite original timespan).
     *
     * You can also optionally specify timespan range with this method.
     *
     * @param int|\DateTime|bool|null $from
     *      int or DateTime: define date range, use specified date
     *      null: define date range, use NULL (-infinity)
     *      false: do not define date range (or translated to NULL if TO is defined)
     * @param int|\DateTime|bool|null $to like $from
     * @return self fluent interface
     */
    public function makeNewTimespan($from = FALSE, $to = FALSE)
    {
        $this->isNewTimespan = TRUE;
        if ($from !== FALSE) $this->setFrom ($from);
        if ($to !== FALSE) $this->setTo($to);
        return $this;
    }
    //</editor-fold>
}